/**
 * 
 */
package org.dragonfire.dasm.preprocessor;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Stack;

import org.dragonfire.dasm.Command;
import org.dragonfire.dasm.CompileException;
import org.dragonfire.dasm.DASM;
import org.dragonfire.dasm.Listing;
import org.dragonfire.dasm.instr.CommandDef;
import org.dragonfire.dasm.preprocessor.blockDefs.IfBlockDef;
import org.dragonfire.dasm.preprocessor.blockDefs.LoopBlockDef;
import org.dragonfire.dasm.preprocessor.blockDefs.MacroBlockDef;
import org.dragonfire.dasm.preprocessor.blockDefs.SwitchBlockDef;
import org.dragonfire.file.BufferedFile;
import org.dragonfire.parser.exceptions.ParseException;
import org.dragonfire.util.DString;
import org.dragonfire.util.TimeProfile;

import com.stevesoft.pat.Regex;

/**
 * @author dandroid
 * @date Dec 7, 2006
 */
public class Preprocessor {

	private DASM compiler = null;

	public Preprocessor(DASM main) {
		compiler = main;
		// register core blocks
		IfBlockDef ib = new IfBlockDef();
		registerBlockDef("if", ib);
		registerBlockDef("ifdef", ib);
		registerBlockDef("ifndef", ib);
		registerBlockDef("macro", new MacroBlockDef());
		registerBlockDef("switch", new SwitchBlockDef());
		LoopBlockDef lb = new LoopBlockDef();
		registerBlockDef("while", lb);
		registerBlockDef("repeat", lb);
		registerBlockDef("loop", lb);
	}

	private Stack<Block> blocks = new Stack<Block>();
	private Stack<BufferedFile> files = new Stack<BufferedFile>();
	private int absLineNum = 1;
	private boolean parsing = true;
	private HashMap<String, BlockDef> blockDefs = new HashMap<String, BlockDef>();
	private Stack<String> modules = new Stack<String>();
	private int pc;
	private boolean list = true;
	private boolean lsFirst = true;
	private boolean isBlocking = true;

	public void setBlocking(boolean blocking) {
		isBlocking = blocking;
	}

	public boolean isBlocking() {
		return isBlocking;
	}

	public void setDefaultListingVisibility(boolean isVisible) {
		list = isVisible;
	}

	public int getPC() {
		return pc;
	}

	public void setPC(int newPC) {
		pc = newPC;
	}

	public void enterModule(String name) {
		modules.push(name.toLowerCase().replaceAll("[^\\w\\.]", ""));
	}

	public String getCurrentModule() {
		String[] modules = getCurrentModuleStack();
		return modules[modules.length - 1];
	}

	public int getAbsLineNum() {
		return absLineNum;
	}

	public void leaveModule() {
		if (modules.size() == 0)
			compiler.warnMsg("ERROR_NO_MODULES");
		else {
			modules.pop();
		}
	}

	public void registerBlockDef(String name, BlockDef def) {
		blockDefs.put(name.toLowerCase(), def);
	}

	public void unregisterBlockDef(String name) {
		blockDefs.remove(name.toLowerCase());
	}

	public void setParsing(boolean parsing) {
		this.parsing = parsing;
	}

	public void setLittleEndian(boolean bool) {
		lsFirst = bool;
	}

	public boolean isLSFirst() {
		return lsFirst;
	}

	public boolean isParsing() {
		return parsing;
	}

	public ArrayList<Listing> preprocessFile(BufferedFile f) {
		long timeStart = System.nanoTime();
		int lineStart = getAbsLineNum();
		ArrayList<Listing> listing = new ArrayList<Listing>();
		if (f == null)
			return listing;
		int releaseDepth = files.size();
		files.push(f);
		while (files.contains(f) && files.size() > releaseDepth && !files.isEmpty()) {
			try {
				if (!getCurrentFile().hasNextLine()) {
					popFile();
					continue;
				}
				listing.addAll(preprocessLine(getCurrentFile().nextLine()));
			} catch (CompileException ce) {
				System.err.println(getCurrentFileName() + ":" + getCurrentLineNum() + ": " + ce.getMessage());
			}
		}
		TimeProfile.addRun("file:" + f.name, System.nanoTime() - timeStart);
		TimeProfile profile = TimeProfile.getProfile("file:" + f.name);
		profile.setCount(getAbsLineNum() - lineStart);
		return listing;
	}

	public String getCurrentFileName() {
		return getCurrentFile() == null ? null : getCurrentFile().name;
	}

	public int getCurrentLineNum() {
		return getCurrentFile() == null ? -1 : getCurrentFile().lineNum + 1;
	}

	public BufferedFile getCurrentFile() {
		return files.isEmpty() ? null : files.peek();
	}

	public void pushFile(BufferedFile f) {
		files.push(f);
	}

	public void popFile() {
		if (!files.isEmpty())
			files.pop();
	}

	public int getBlockDepth() {
		return blocks.size();
	}

	public Block getCurrentBlock() {
		return blocks.isEmpty() ? null : blocks.peek();
	}

	public void pushBlock(Block b) {
		blocks.push(b);
	}

	public void popBlock() {
		blocks.pop();
	}

	public void define(String name, String value) {
		getCompiler().define(name, getCurrentModule(), value);
	}

	public void undefine(String name) {
		getCompiler().undefine(name, getCurrentModule());
	}

	public DASM getCompiler() {
		return compiler;
	}

	public String[] getCurrentModuleStack() {
		if (modules.size() == 0 || !modules.get(0).equals(""))
			modules.add(0, "");
		return modules.toArray(new String[modules.size()]);
	}

	public boolean getListingVisible() {
		return list;
	}

	public ArrayList<Listing> preprocessLine(String line) {
		line = getCompiler().prepLine(line);
		absLineNum++;
		ArrayList<Listing> list = new ArrayList<Listing>();
		if (line.length() == 0)
			return list;
		boolean freshBlock = false;
		try {
			if (DString.splitTopLevel(line, '=').length == 2) {
				String[] parts = DString.splitTopLevel(line, '=');
				parts[0] = DString.trim(parts[0]);
				if (parts[0].matches(DASM.WORD_REGEX)) {
					line = "#DEFINE " + parts[0] + " " + parts[1];
					getCompiler().prepLine(line);
				}
			}
			{
				Regex labelR = new Regex("^\\s*(" + DASM.LABEL_REGEX + ")\\s*:\\s*(.*)");
				while (labelR.search(line)) {
					define(labelR.stringMatched(1), "" + getPC());
					line = labelR.stringMatched(2);
					if (line == null)
						return list;
				}
			}
			Command cmd = null;
			Regex r = new Regex(DASM.COMMAND_REGEX);
			if (r.search(line)) {
				String paramsRaw = r.stringMatched(2);
				if (paramsRaw == null)
					paramsRaw = r.stringMatched(3);
				cmd = new Command(r.stringMatched(1), paramsRaw);
			}
			if (cmd == null)
				throw new CompileException("ERROR_MALFORMED_COMMAND");
			else if (cmd.isDirective() && isBlocking() && blockDefs.containsKey(cmd.getName().toLowerCase())) {
				Block b = blockDefs.get(cmd.getName().toLowerCase()).createBlock(this, cmd);
				b.setInControl(isParsing());
				b.setFile(getCurrentFileName());
				b.setLine(getCurrentLineNum());
				pushBlock(b);
				freshBlock = true;
				getCurrentBlock().parsePersonalDirective(this, cmd);
			}
			else if (cmd.isDirective() && getCurrentBlock() != null
					&& getCurrentBlock().isPersonalDirective(cmd.getName().toLowerCase())
					&& getCurrentBlock().isInControl()) {
				getCurrentBlock().parsePersonalDirective(this, cmd);
			}
			else if (cmd.isDirective() && !blocks.isEmpty()
					&& getCurrentBlock().getEnd().equalsIgnoreCase(cmd.getName()) && !getCurrentBlock().isInControl()) {
				popBlock();
			}
			else if (!freshBlock && !isBlocking() && getCurrentBlock() != null)
				getCurrentBlock().currLine(line);
			else if (cmd.isDirective() && isParsing()) {
				list = getCompiler().getDM().pass1(cmd);
			}
			else if (isParsing() && !cmd.isDirective()) {
				if (line.matches("\\s*"))
					return list;
				if (DString.splitTopLevel(line, DASM.NEWLINE_CHAR).length > 1) {
					for (String subLine : DString.splitTopLevel(line, DASM.NEWLINE_CHAR))
						list.addAll(preprocessLine(subLine));
					return list;
				}
				CommandDef c = getCompiler().getInstructions().getCommand(cmd.getName(), cmd.getParams());
				list = c.pass1(cmd);
			}
		} catch (StackOverflowError se) {
			CompileException ce = new CompileException("ERROR_STACK_OVERFLOW");
			getCompiler().prepCompileException(ce);
			getCompiler().error(ce);
			if (getCompiler().isDebugging()) {
				se.printStackTrace();
			}
		} catch (ParseException pe) {
			CompileException ce = new CompileException("ERROR_PARSING", pe.getMessage());
			getCompiler().prepCompileException(ce);
			getCompiler().error(ce);
		} catch (CompileException e) {
			getCompiler().prepCompileException(e);
			getCompiler().error(e);
		} catch (Throwable e) {
			CompileException ce = new CompileException("ERROR_INTERNAL", DASM.AUTHOR);
			getCompiler().prepCompileException(ce);
			getCompiler().error(ce);
			if (getCompiler().getBooleanParam("debug")) {
				System.err.println("Pass 1:");
				e.printStackTrace(System.err);
			}
		}
		if (list == null)
			list = new ArrayList<Listing>();
		for (Listing l : list) {
			prepListing(l);
			setPC(getPC() + l.getSize());
		}
		return list;
	}

	/**
	 * @param list
	 */
	private void prepListing(Listing list) {
		if (list.getAbsLine() == -1)
			list.setAbsLine(getAbsLineNum());
		if (list.getFileName() == null)
			list.setFileName(getCurrentFileName());
		if (list.getModuleStack() == null)
			list.setModuleStack(getCurrentModuleStack());
		if (list.getPc() == -1)
			list.setPc(getPC());
		list.setVisible(getListingVisible()
				|| (getCompiler().getBooleanParam("show-empty-listings") && list.getSize() == 0));
		list.setLSFirst(isLSFirst());
		if (list.getLineNum() == -1)
			list.setLineNum(getCurrentLineNum());
		if (list.getCommand() == null)
			list.setCommand(new Command("???", new String[0]));
	}

	/**
	 * Convienence method
	 * 
	 * @param equation
	 * @return
	 */
	public String eval(String equation) {
		return compiler.eval(equation);
	}
}
